#!/bin/bash
# ==============================
# 脚本功能：ssh 连接管理工具
# 开发作者：Allen
# 依赖命令：sed | awk | sshspass
# ==============================

# ========== 预定义变量 ========== # 定义一些全局变量或配置
# sshs 数据主目录
sshs_dir=~/.sshs
# 列分隔符，默认为“丨”(拼音为“gun”，灵感来自于网络上一个程序员的梗，列分隔符不能为常用的字符，因此采用这个生僻字)
column_split="丨"
# 布尔值表示
boolean_true="true"; boolean_false="false"

# ========== 脚本预处理 ========== # 处理一些前期行为
# sshs_profile 文件是保存 ssh 信息的文件
# sshs_profile 文件的格式：<id>丨<host>丨<port>丨<user>丨<pwd base64>丨<desc base64>；以 # 开头的是注释行；
sshs_profile=$sshs_dir/sshs_profile
# 获取到本脚本的名称
script_name=$(basename $0)

# ========== 预定义函数 ========== # 定义一些功能函数
# 打印帮助信息
print_usage () {
    echo "usage: ${script_name} <command> [<args>]" >&2
    echo "            list [<index>]" >&2
    echo "            add [-h <host>] [-P <port>] [-u <user>] [-p <password>] [-m <comment>]" >&2
    echo "            update <index> [-h <host>] [-P <port>] [-u <user>] [-p <password>] [-m <comment>]" >&2
    echo "            remove <index>" >&2
    echo "            <index>" >&2
    echo "" >&2
    echo "These are common ${script_name} commands used in various situations:" >&2
    echo "   list       List all ssh info" >&2
    echo "   add        Add a ssh info" >&2
    echo "   update     Update a ssh info" >&2
    echo "   remove     Remove a ssh info" >&2
    echo "   reset      Remove all ssh info" >&2
    echo "   <index>    Connect of the index" >&2
    return 0
}
# 打印日志
log () {
    # type: debug info warn error
    local readonly type=$1; local readonly line=$2; local readonly msg=$3
    
    # type 大写处理
    local readonly TYPE=$(echo $type | awk '{print toupper($0)}')
    echo "[$TYPE] $msg <$line>" >&2
    return 0
}
# 截断参数列表的第一个
# 比如“1 2 3 4”，截断后得到“2 3 4”
trunc_params_first () {
    local readonly args=$*

    echo $args | awk '{$1=""; print $0}'
    return 0
}
# 获取字符串的长度，全角字符按2个长度算
get_str_len () {
    local readonly str=$1

    local readonly len1=${#str}; local readonly len2=$(echo "$str" | awk '{print length($0)}')
    local tmp=$(expr $len2 - $len1); tmp=$(expr $tmp / 2)
    echo $(expr $len1 + $tmp)

    return 0
}
# 字符串尾部缺省处理
omit_str () {
    # 要处理的字符串
    local readonly str=$1
    # 要处理的长度
    local readonly len=$2

    # 缺省填充值
    local readonly fill="..."
    echo "${str:0:$(expr $len - ${#fill})}$fill"

    return 0
}
# 填充一个字符串
# 针对一个字符串，不足 len 长度的位置在前面或后面用 rep 字符串来填充
fill_str () {
    # 填充类型：front-前部; tail-尾部
    local readonly type=$1
    # 需要被填充的字符串
    local readonly str=$2
    # 填充后长度
    local readonly len=$3
    # 用什么字符串来填充
    local readonly rep=$4

    # 字符串实际长度
    local readonly len_str=${#str}

    # 如果字符串长度等于填充后长度，则直接输出
    if [ $len -eq $len_str ]; then echo "$str"
    # 如果字符串长度大于填充长度，则在尾部用...表示
    elif [ $len -lt $len_str ]; then omit_str "$str" "$len"
    # 如果字符串长度小于填充长度，则按要求填充
    else
        local readonly fill_len=$(expr $len - $len_str)
        # 计算填充值
        local fill=""; for ((i=1;i<=$fill_len;i++)); do fill="$fill$rep"; done
        if [ "$type" == "front" ]; then echo "$fill$str"; else echo "$str$fill"; fi
    fi

    return 0
}
# 打印一行 数据/标题/分割线
print_line () {
    local readonly index=$1
    local readonly host=$2
    local readonly port=$3
    local readonly user=$4
    local readonly desc=$5

    echo "    $index  $host  $port  $user  $desc"
}
# 填充一行数据
fill_line () {
    local index=$1
    local host=$3
    local port=$4
    local user=$5
    local desc=$7

    # 列宽数组
    column_width=(5 15 5 12 21)
    # base64 解码
    desc=$(echo $desc | base64 -D)

    index=$(fill_str front "$index" ${column_width[0]} " ")
    host=$(fill_str tail "$host" ${column_width[1]} " ")
    port=$(fill_str front "$port" ${column_width[2]} " ")
    user=$(fill_str tail "$user" ${column_width[3]} " ")
    desc=$(fill_str tail "$desc" ${column_width[4]} " ")
    print_line "$index"  "$host"  "$port"  "$user"  "$desc"
    return 0
}
# 打印填充好的表头
print_list_header () {
    # 列宽数组
    column_width=(5 15 5 12 21)

    local index=$(fill_str tail "Index" ${column_width[0]} " ")
    local host=$(fill_str tail "Host" ${column_width[1]} " ")
    local port=$(fill_str tail "Port" ${column_width[2]} " ")
    local user=$(fill_str tail "User" ${column_width[3]} " ")
    local desc=$(fill_str tail "Desc" ${column_width[4]} " ")
    print_line "$index"  "$host"  "$port"  "$user"  "$desc" >&2
    index=$(fill_str tail "-" ${column_width[0]} "-")
    host=$(fill_str tail "-" ${column_width[1]} "-")
    port=$(fill_str tail "-" ${column_width[2]} "-")
    user=$(fill_str tail "-" ${column_width[3]} "-")
    desc=$(fill_str tail "-" ${column_width[4]} "-")
    print_line "$index"  "$host"  "$port"  "$user"  "$desc" >&2
    return 0
}
# 筛选出有效的内容
# 获取 sshs_profile 中的有效 ssh 数据
screen_available () {
    if [ -f $sshs_profile ]; then cat $sshs_profile | grep -v "^\s*$" | grep -v "^\s*#.*"; fi
    return 0
}
# 获取当前最大id
max_id () {
    local readonly available=$(screen_available)

    if [ ! "$available" ]; then echo 0; else screen_available | awk -F "${column_split}" 'END {print $1}'; fi
    return 0
}
# 根据 index 获取一行数据(包含 index)
get_line_with_index_by_index () {
    local readonly index=$1

    screen_available | cat -n | awk '{print $1"'"$column_split"'"$2}' | grep -e "^$index$column_split"
    return 0
}
# 根据 index 获取一行数据(不包含 index)
get_line_without_index_by_index () {
    local readonly index=$1

    screen_available | cat -n | grep -E "^\s+$index\s+" | awk '{print $2}'
    return 0
}
# 根据 index 获取 id
get_id_by_index () {
    local readonly index=$1
    get_line_with_index_by_index $index | awk -F "$column_split" '{print $2}'
    return 0
}
# 判断是否是数字，返回 boolean
is_number () {
    local readonly value=$1

    if [[ "$value" =~ ^[0-9]+$ ]]; then echo "$boolean_true"; else echo "$boolean_false"; fi
    return 0
}
# 判断是否是ip，返回 boolean
is_ip () {
    local readonly value=$1

    if [[ "$value" =~ ^([1-9][0-9]{0,2}\.){3}[1-9][0-9]{0,2}$ ]]; then echo "$boolean_true"; else echo "$boolean_false"; fi
    return 0
}
# 判断是否是域名，返回 boolean
is_host_name () {
    local readonly value=$1

    if [[ "$value" =~ ^[0-9a-zA-Z\._-]+$ ]]; then echo "$boolean_true"; else echo "$boolean_false"; fi
    return 0
}
# 判断是否是正确的端口
is_port () {
    local readonly value=$1

    if [ $(is_number $value) == "$boolean_true" ]; then if [ $value -le 65535 ]; then echo "$boolean_true"; return 0; fi; fi
    echo "$boolean_false"
    return 0
}
# 列出所有 ssh 信息
list_all () {
    print_list_header

    if [ ! -f $sshs_profile ]; then echo -e "\t<Nothing>" >&2; return 0; fi

    local readonly available=$(screen_available)
    if [ ! "$available" ]; then echo -e "\t<Nothing>" >&2
    else
        export -f fill_line
        screen_available | \
        cat -n | \
        awk '{print $1"'"$column_split"'"$2}' | \
        # awk 处理多行时记得执行 close
        awk -F "$column_split" '{"fill_line \""$1"\" \""$2"\" \""$3"\" \""$4"\" \""$5"\" \""$6"\" \""$7"\"" | getline line; print line; close("fill_line \""$1"\" \""$2"\" \""$3"\" \""$4"\" \""$5"\" \""$6"\" \""$7"\"")}'
    fi

    return 0
}
# 按照 index 列出 ssh 信息
list_index () {
    local readonly index=$1

    print_list_header

    local readonly count=$(screen_available | wc -l)
    if [ $index -gt $count ]; then echo -e "\t<Nothing>" >&2; fi

    export -f fill_line
    screen_available | \
    cat -n | \
    awk '{print $1"'"${column_split}"'"$2}' | \
    grep -e "^$index${column_split}" | \
    awk -F "$column_split" '{"fill_line \""$1"\" \""$2"\" \""$3"\" \""$4"\" \""$5"\" \""$6"\" \""$7"\"" | getline line; print line; close("fill_line \""$1"\" \""$2"\" \""$3"\" \""$4"\" \""$5"\" \""$6"\" \""$7"\"")}'

    return 0
}
# 执行 list 命令
list () {
    # 检查参数个数
    if [ $# -gt 1 ]; then log warn $LINENO "list: too many parameters"; fi
    # 如果list没有参数，则列出所有
    if [ $# -eq 0 ]
    then
        list_all
    else
        list_index $1
    fi

    return 0
}
# 执行 add 命令
add () {
    # 解析参数
    while [ $# -ge 2 ]
    do
        case $1 in
        -h) local readonly host=$2; shift 2;;
        -P) local port=$2; shift 2;;
        -u) local readonly user=$2; shift 2;;
        -p) local pwd=$2; shift 2;;
        -m) local desc=$2; shift 2;;
        *) log error $LINENO "add: Invalid option '$1'."; exit 2;;
        esac
    done

    # 参数检查
    if [ ! $host ]; then
        log error $LINENO "add: Missing option '-h'."
        exit 3
    fi
    if [ ! $user ]; then
        log error $LINENO "add: Missing option '-u'."
        exit 3
    fi
    if [ ! $pwd ]; then
        log error $LINENO "add: Missing option '-p'." >&2;
        exit 3
    fi

    # 参数处理
    if [ ! $port ]; then port=22; fi
    # base64 编码
    pwd=$(echo $pwd | base64)
    if [ $desc ]; then desc=$(echo $desc | base64); fi

    local readonly m_id=$(max_id)
    local readonly id=$(expr $m_id + 1)
    if [ ! -d $sshs_dir ]; then mkdir $sshs_dir; fi
    if [ ! -f $sshs_profile ]; then touch $sshs_profile; fi
    echo "$id${column_split}$host${column_split}$port${column_split}$user${column_split}$pwd${column_split}$desc" >> $sshs_profile

    return 0
}
# 执行 update 命令
update () {
    local readonly index=$1

    if [ ! $index ]; then log error $LINENO "remove: missing parameter '<index>'"; exit 1; fi
    
    shift 1
    # 解析参数
    while [ $# -ge 1 ]
    do
        case $1 in
        -h) local readonly host=$2; shift 2;;
        -P) local readonly port=$2; shift 2;;
        -u) local readonly user=$2; shift 2;;
        -p) local readonly pwd=$2; shift 2;;
        -m) local readonly desc=$2; shift 2;;
        *) log error $LINENO "add: invalid option '$1'"; exit 2
        esac
    done

    # 旧数据
    local readonly line_old=$(get_line_without_index_by_index $index)

    # 新数据初始化为旧数据
    local readonly id_new=$(echo $line_old | awk -F "$column_split" '{print $1}')
    local host_new=$(echo $line_old | awk -F "$column_split" '{print $2}')
    local port_new=$(echo $line_old | awk -F "$column_split" '{print $3}')
    local user_new=$(echo $line_old | awk -F "$column_split" '{print $4}')
    local pwd_new=$(echo $line_old | awk -F "$column_split" '{print $5}')
    local desc_new=$(echo $line_old | awk -F "$column_split" '{print $6}')

    # 新数据
    if [ $host ]; then host_new=$host; fi
    if [ $port ]; then port_new=$host; fi
    if [ $user ]; then user_new=$host; fi
    if [ $pwd ]; then pwd_new=$(echo $pwd | base64); fi
    if [ $desc ]; then desc_new=$(echo $desc | base64); fi

    local line_new="$id_new$column_split$host_new$column_split$port_new$column_split$user_new$column_split$pwd_new$column_split$desc_new"
    line_new=${line_new//\//\\\/}
    
    # 替换
    sed -i '' "s/^$id_new$column_split.*$/$line_new/g" $sshs_profile >&2

    return 0
}
# 执行 remove 命令
remove () {
    local readonly index=$1

    if [ ! $index ]; then log error $LINENO "remove: missing parameter '<index>'"; exit 1; fi
    # 检查参数个数
    if [ $# -ne 1 ]; then log warn $LINENO "remove: too many parameters"; fi

    local readonly id=$(get_id_by_index $index)
    # Mac 中使用 sed 命令 -i 选项时，-i 后面要带上 ''
    if [ $id ]; then sed -i '' '/^'"$id$column_split"'/d' "$sshs_profile"; fi

    return 0
}
# 执行 reset 命令
reset () {
    # 检查参数个数
    if [ $# -gt 0 ]; then log warn $LINENO "reset: doesn't need any parameters."; fi
    if [ -f $sshs_profile ]; then cat /dev/null > $sshs_profile; fi
    return 0
}
# 执行 connect 命令
connect () {
    local readonly index=$1
    local readonly count=$(screen_available | wc -l)
    if [ $index -gt $count ]; then log warn $LINENO "connect: not exist index $index"; exit 61; fi
    local readonly line=$(get_line_without_index_by_index $index)
    local readonly host=$(echo $line | awk -F "$column_split" '{print $2}')
    local readonly port=$(echo $line | awk -F "$column_split" '{print $3}')
    local readonly user=$(echo $line | awk -F "$column_split" '{print $4}')
    local pwd=$(echo $line | awk -F "$column_split" '{print $5}')

    pwd=$(echo $pwd | base64 -D)

    sshpass &> /dev/null
    if [ $? -ne 0 ]; then log error $LINENO "connect: sshpass command is not supported"; return 62; fi
    sshpass -p "$pwd" ssh $user@$host
    
    return 0
}

# ========== 主程序区 ==========
# 必须要 export 该函数，否则无法在 awk 中使用
export -f fill_str; export -f print_line; export -f omit_str
# 如果没有参数则打印帮助信息
if [ 0 -eq $# ]; then print_usage; exit 0; fi
# 根据不同命令执行
case "$1" in
list)
    list $(trunc_params_first $*)
    ;;
add)
    add $(trunc_params_first $*)
    ;;
update)
    update $(trunc_params_first $*)
    ;;
remove)
    remove $(trunc_params_first $*)
    ;;
reset)
    reset $(trunc_params_first $*)
    ;;
*)  if [ $(is_number $1) == $boolean_true ]; then connect $*
    else
        log error $LINENO "*: unsupported option '$1'"
        exit 70
    fi
    ;;
esac
